﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading;

namespace Pscx.TypeConverters
{
    public class ScriptBlockDelegateTypeConverter : PSTypeConverter
    {
        public override bool CanConvertFrom(object sourceValue, Type destinationType)
        {
            return false;
        }

        public override object ConvertFrom(object sourceValue, Type destinationType, IFormatProvider formatProvider,
                                           bool ignoreCase)
        {
            throw new InvalidCastException();
        }

        public override bool CanConvertTo(object sourceValue, Type destinationType)
        {
            Debug.Assert(destinationType != null, "destinationType != null");
            if (sourceValue.GetType() == typeof(ScriptBlock))
            {
                if (typeof(Delegate).IsAssignableFrom(destinationType) && !destinationType.IsAbstract)
                {
                    return true;
                }
            }
            return false;
        }

        public override object ConvertTo(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase)
        {
            if (sourceValue != null)
            {
                Debug.Assert(sourceValue.GetType() == typeof(ScriptBlock), "sourceValue == [scriptblock]");
                Debug.Assert(destinationType != null, "destinationType != null");

                MethodInfo invoke = destinationType.GetMethod("Invoke");
                Debug.Assert(invoke != null, "delegate invoke != null");

                ParameterInfo[] parameters = invoke.GetParameters();
                Type returnType = invoke.ReturnParameter.ParameterType;

                List<Type> args = new List<Type>();
                args.Add(typeof (ScriptBlock)); // first argument is instance

                foreach (ParameterInfo parameter in parameters)
                {
                    args.Add(parameter.ParameterType);
                }

                DynamicMethod method =
                    new DynamicMethod(String.Empty, returnType, args.ToArray(), typeof (object), false);
                ILGenerator ilgen = method.GetILGenerator();

                ilgen.Emit(OpCodes.Ldarg_0);
                ilgen.Emit(OpCodes.Newarr, typeof (object));

                for (int index = 1; index < args.Count; index++)
                {
                    ilgen.Emit(OpCodes.Dup);
                    ilgen.Emit(OpCodes.Ldc_I4, index - 1);
                    ilgen.Emit(OpCodes.Ldarg, index);

                    if (args[index].IsValueType)
                    {
                        ilgen.Emit(OpCodes.Box);
                    }
                    ilgen.Emit(OpCodes.Stelem, typeof (object));
                }

                ilgen.Emit(OpCodes.Call, typeof (ScriptBlock).GetMethod("InvokeReturnAsIs"));

                if (returnType == typeof (void))
                {
                    ilgen.Emit(OpCodes.Pop);
                } else
                {
                    MethodInfo convert =
                        typeof (LanguagePrimitives).GetMethod("ConvertTo",
                            new Type[] {typeof (object), typeof (Type)});

                    MethodInfo typeFromHandle = typeof (Type).GetMethod("GetTypeFromHandle");
                    ilgen.Emit(OpCodes.Ldtoken, returnType);
                    ilgen.Emit(OpCodes.Call, typeFromHandle);
                    ilgen.Emit(OpCodes.Call, convert);
                }
                ilgen.Emit(OpCodes.Ret);

                //$dynMethod.CreateDelegate($type, $scriptBlock)
                return method.CreateDelegate(destinationType, sourceValue);
            }
            else
            {
                // source value null
                throw new ArgumentNullException("sourceValue");
            }
        }               
    }

    internal static class ScriptRunner
    {
        internal class ScriptInfo
        {
            internal string ScriptText;
            internal object[] Arguments;
            internal object ReturnValue;
        }

        //private readonly ScriptBlock _scriptBlock;
        //private readonly bool _suppressNoRunspaceError;
        //private readonly bool _useStaThread; // TODO

        //internal ScriptRunner(ScriptBlock scriptBlock, bool suppressNoRunspaceError)
        //{
        //    this._scriptBlock = _scriptBlock;
        //    this._suppressNoRunspaceError = _suppressNoRunspaceError;
        //}

        internal static PSObject Run(string script, params object[] args)
        {
            try
            {

            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.ToString(), "ScriptRunner::Run");
                //Runspace.DefaultRunspace.CreateNestedPipeline()
            }
            return null;
        }

        internal static PSObject Run(ScriptBlock block, params object[] args)
        {
            return null;
        }

        internal static PSObject RunSta(string script, params object[] args)
        {
            try
            {
                ScriptInfo info = new ScriptInfo();
                info.ScriptText = script;
                info.Arguments = args;

                Thread t = new Thread(new ParameterizedThreadStart(RunScriptBlockInSta));
                t.SetApartmentState(ApartmentState.STA);
                t.Start(info);
                t.Join();
                
                return PSObject.AsPSObject(info.ReturnValue);
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.ToString(), "ScriptRunner::RunSta");
                return null;
            }
        }

        private static void RunScriptBlockInSta(object state)
        {
            ScriptInfo info = (ScriptInfo)state;
            string script = info.ScriptText;
            object[] args = info.Arguments;

            using (Runspace rs = RunspaceFactory.CreateRunspace())
            {
                // Must set the default runspace
                Runspace.DefaultRunspace = rs;
                rs.Open();
                EngineIntrinsics engine = rs.SessionStateProxy.GetVariable("ExecutionContext") as EngineIntrinsics;
                Debug.Assert(engine != null, "engine != null");

                // Now run the scriptblock on the Main (STA) thread
                // instead of the engine thread which is MTA
                ScriptBlock sb = engine.InvokeCommand.NewScriptBlock(script);
                info.ReturnValue = sb.InvokeReturnAsIs(args);
            }
        }
        //internal TReturn Invoke(params object[] arguments)
        //{
        //    Collection<PSObject> results = _scriptBlock.Invoke(arguments);
        //    Debug.Assert(results.Count == 1, "results.Count == 1");

        //    return (TReturn)results[0].BaseObject;
        //}
    }
}
